using System;
using System.IO;
using System.Threading;

namespace PTP.Usb
{
  public abstract class PipeStream : Stream
  {
    protected IPipe inPipe;
    protected IPipe outPipe;

    // read and write uses same buffer. The implementation is not thread safe.
    private byte[] dataBuffer = null;

    // Amount of data that is read from pipe to this buffer
    private int inBufferDataCount = 0;
    // Amount of data that is already read from buffer
    private int inBufferPosition = 0;
    // Amount of data that is written to buffer
    private int outBufferDataCount = 0;

    public PipeStream(IPipe inPipe, IPipe outPipe)
    {
      if (inPipe == null)
        throw new ArgumentNullException("inPipe");
      if (outPipe == null)
        throw new ArgumentNullException("outPipe");

      this.inPipe = inPipe;
      this.outPipe = outPipe;
    }

    public override bool CanRead
    {
      get { return true; }
    }

    public override bool CanSeek
    {
      get { return true; }
    }

    public override bool CanWrite
    {
      get { return true; }
    }

    public override void Flush()
    {
      this.outPipe.TransferData(dataBuffer, 0, outBufferDataCount);
      this.outBufferDataCount = 0;
    }

    public override long Length
    {
      get { throw new InvalidOperationException("USBH_PipeStream doesn't support Length"); }
    }

    public override long Position
    {
      get
      {
        throw new InvalidOperationException("USBH_PipeStream doesn't support Position");
      }
      set
      {
        throw new InvalidOperationException("USBH_PipeStream doesn't support Position");
      }
    }

    protected abstract int getBufferSize();

    protected void ensureBuffer()
    {
      // Reserve space for one packet
      if (this.dataBuffer == null)
        this.dataBuffer = new byte[this.getBufferSize()];
    }

    public override int ReadByte()
    {
      readImpl(null, 0, 1); //ensure that there's data in the buffer
      return (int)this.dataBuffer[this.inBufferPosition - 1];
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
      if (buffer == null)
        throw new ArgumentNullException("buffer");
      return this.readImpl(buffer, offset, count);
    }

    protected int readImpl(byte[] buffer, int offset, int count)
    {
      ensureBuffer();

      int dataRead = 0;

      while (dataRead < count)
      {
        //some data available already in the buffer?
        if (this.inBufferPosition < this.inBufferDataCount)
        {
          int amountToCopy = count - dataRead;

          //Calculate amount that is copied from buffer
          {
            int amountAvail = this.inBufferDataCount - this.inBufferPosition;
            if (amountToCopy > amountAvail)
              amountToCopy = amountAvail;
          }

          // ReadByte and Seek implementations call read with null buffer
          if (buffer != null)
            Array.Copy(this.dataBuffer, this.inBufferPosition, buffer, offset + dataRead, amountToCopy);

          this.inBufferPosition += amountToCopy;
          dataRead += amountToCopy;
        }
        else if (this.inBufferPosition == this.inBufferDataCount)
        {
          //Get new data and try again in the next iteration
          try
          {
            this.inBufferDataCount = this.inPipe.TransferData(this.dataBuffer, 0, this.dataBuffer.Length);
            this.inBufferPosition = 0;

            //If there's no data yet, wait a while
            if (this.inBufferDataCount == 0)
              Thread.Sleep(50);
#if DEBUG
            else
            {
              if (PTP.Core.DebugUtil.TraceEnabled)
              {
                //write bytes to debugger
                PTP.Core.DebugUtil.Write(PTP.Core.DebugUtil.ReadBytesCategory, PTP.Core.DebugUtil.ToHexDigits(this.dataBuffer, this.inBufferPosition, this.inBufferDataCount));
              }
            }
#endif
          }
          catch (Exception)
          {
            // Stream documentation: if an exception occurs, the current position within the stream remains unchanged..
            // Implementation: we must not throw exception here. Some data might be already copied. Return the data that is read so far.
            return dataRead;
          }
        }
        else
          throw new Exception("inBufferPosition: " + this.inBufferPosition + ", inBufferDataCount: " + this.inBufferDataCount);
      }

      return dataRead;
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
      if (offset < 0)
        throw new ArgumentException("offset must be positive number");
      if (origin != SeekOrigin.Current)
        throw new ArgumentException("only SeekOrigin.Current is supported");

      readImpl(null, 0, (int)offset);

      return -1; //just some value..
    }

    public override void SetLength(long value)
    {
      throw new InvalidOperationException("USBH_PipeStream doesn't support SetLength");
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
      if (buffer == null)
        throw new ArgumentNullException("buffer");
      writeImpl(buffer, offset, count);
    }

    protected void writeImpl(byte[] buffer, int offset, int count)
    {
      ensureBuffer();

      int currentPosition = offset;
      while (currentPosition < offset + count)
      {
        // space available in buffer?
        if (this.outBufferDataCount < this.dataBuffer.Length)
        {
          int amountToCopy = count - currentPosition;

          //Calculate amount that is copied from buffer
          {
            int amountAvail = this.dataBuffer.Length - this.outBufferDataCount;
            if (amountToCopy > amountAvail)
              amountToCopy = amountAvail;
          }

          // ReadByte and Seek implementations call read with null buffer
          if (buffer != null)
            Array.Copy(buffer, currentPosition, this.dataBuffer, this.outBufferDataCount, amountToCopy);

          this.outBufferDataCount += amountToCopy;
          currentPosition += amountToCopy;
        }
        else 
        {
          this.Flush();
        }
      }
    }

    public override void WriteByte(byte value)
    {
      writeImpl(null, 0, 1);
      this.dataBuffer[this.outBufferDataCount - 1] = value;
    }

    protected override void Dispose(bool disposing)
    {
      //Release managed resources
		if (this.dataBuffer != null)
		{
			this.dataBuffer = null;
		}

		if (this.inPipe != null)
		{
			this.inPipe.Dispose();
			this.inPipe = null;
		}

		if (this.outPipe != null)
		{
			this.outPipe.Dispose();
			this.outPipe = null;
		}

      base.Dispose(disposing);
    }
  }
}
